Skip to content

[clang-tidy] add modernize-use-constexpr check #146553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

5chmidti
Copy link
Contributor

@5chmidti 5chmidti commented Jul 1, 2025

This check finds all functions and variables that can be declared as
constexpr, using the specified standard version to check if the
requirements are met.

Fixes #115622

@llvmbot
Copy link
Member

llvmbot commented Jul 1, 2025

@llvm/pr-subscribers-clang-tidy

Author: Julian Schmidt (5chmidti)

Changes

This check finds all functions and variables that can be declared as
constexpr, using the specified standard version to check if the
requirements are met.

Fixes #115622


Patch is 95.12 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/146553.diff

9 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/misc/CMakeLists.txt (+1)
  • (added) clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp (+936)
  • (added) clang-tools-extra/clang-tidy/misc/ConstexprCheck.h (+43)
  • (modified) clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp (+3)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/list.rst (+1)
  • (added) clang-tools-extra/docs/clang-tidy/checks/misc/constexpr.rst (+40)
  • (added) clang-tools-extra/test/clang-tidy/checkers/misc/constexpr-cxx20.cpp (+36)
  • (added) clang-tools-extra/test/clang-tidy/checkers/misc/constexpr-ref.cpp (+383)
  • (added) clang-tools-extra/test/clang-tidy/checkers/misc/constexpr.cpp (+562)
diff --git a/clang-tools-extra/clang-tidy/misc/CMakeLists.txt b/clang-tools-extra/clang-tidy/misc/CMakeLists.txt
index fd7affd22a463..4535df3451c95 100644
--- a/clang-tools-extra/clang-tidy/misc/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/misc/CMakeLists.txt
@@ -19,6 +19,7 @@ set_target_properties(genconfusable PROPERTIES FOLDER "Clang Tools Extra/Sourceg
 
 add_clang_library(clangTidyMiscModule STATIC
   ConstCorrectnessCheck.cpp
+  ConstexprCheck.cpp
   CoroutineHostileRAIICheck.cpp
   DefinitionsInHeadersCheck.cpp
   ConfusableIdentifierCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp b/clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp
new file mode 100644
index 0000000000000..270ac1dd0fa48
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp
@@ -0,0 +1,936 @@
+//===--- ConstexprCheck.cpp - clang-tidy ----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "ConstexprCheck.h"
+#include "../utils/ASTUtils.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclBase.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/OperationKinds.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/AST/Stmt.h"
+#include "clang/AST/StmtCXX.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/ASTMatchers/ASTMatchersInternal.h"
+#include "clang/ASTMatchers/ASTMatchersMacros.h"
+#include "clang/Basic/LangOptions.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Basic/Specifiers.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Support/Casting.h"
+#include <cstddef>
+#include <functional>
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::misc {
+
+namespace {
+AST_MATCHER(FunctionDecl, locationPermitsConstexpr) {
+  const bool IsInMainFile =
+      Finder->getASTContext().getSourceManager().isInMainFile(
+          Node.getLocation());
+
+  if (IsInMainFile && Node.hasExternalFormalLinkage())
+    return false;
+  if (!IsInMainFile && !Node.isInlined())
+    return false;
+
+  return true;
+}
+
+AST_MATCHER(Expr, isCXX11ConstantExpr) {
+  return !Node.isValueDependent() &&
+         Node.isCXX11ConstantExpr(Finder->getASTContext());
+}
+
+AST_MATCHER(DeclaratorDecl, isInMacro) {
+  const SourceRange R =
+      SourceRange(Node.getInnerLocStart(), Node.getLocation());
+
+  return Node.getLocation().isMacroID() || Node.getEndLoc().isMacroID() ||
+         utils::rangeContainsMacroExpansion(
+             R, &Finder->getASTContext().getSourceManager()) ||
+         utils::rangeIsEntirelyWithinMacroArgument(
+             R, &Finder->getASTContext().getSourceManager());
+}
+
+AST_MATCHER(Decl, hasNoRedecl) {
+  // There is always the actual declaration
+  return !Node.redecls().empty() &&
+         std::next(Node.redecls_begin()) == Node.redecls_end();
+}
+
+AST_MATCHER(Decl, allRedeclsInSameFile) {
+  const SourceManager &SM = Finder->getASTContext().getSourceManager();
+  const SourceLocation L = Node.getLocation();
+  for (const Decl *ReDecl : Node.redecls()) {
+    if (!SM.isWrittenInSameFile(L, ReDecl->getLocation()))
+      return false;
+  }
+  return true;
+}
+
+AST_MATCHER(FunctionDecl, isConstexprSpecified) {
+  return Node.isConstexprSpecified();
+}
+
+bool satisfiesConstructorPropertiesUntil20(const CXXConstructorDecl *Ctor,
+                                           ASTContext &Ctx) {
+  const CXXRecordDecl *Rec = Ctor->getParent();
+  llvm::SmallPtrSet<const RecordDecl *, 8> Bases{};
+  for (const CXXBaseSpecifier Base : Rec->bases()) {
+    Bases.insert(Base.getType()->getAsRecordDecl());
+  }
+  llvm::SmallPtrSet<const FieldDecl *, 8> Fields{Rec->field_begin(),
+                                                 Rec->field_end()};
+  llvm::SmallPtrSet<const FieldDecl *, 4> Indirects{};
+
+  for (const CXXCtorInitializer *const Init : Ctor->inits()) {
+    const Type *InitType = Init->getBaseClass();
+    if (InitType && InitType->isRecordType()) {
+      const auto *ConstructingInit =
+          llvm::dyn_cast<CXXConstructExpr>(Init->getInit());
+      if (ConstructingInit &&
+          !ConstructingInit->getConstructor()->isConstexprSpecified())
+        return false;
+    }
+
+    if (Init->isBaseInitializer()) {
+      Bases.erase(Init->getBaseClass()->getAsRecordDecl());
+      continue;
+    }
+
+    if (Init->isMemberInitializer()) {
+      const FieldDecl *Field = Init->getMember();
+
+      if (Field->isAnonymousStructOrUnion())
+        Indirects.insert(Field);
+
+      Fields.erase(Field);
+      continue;
+    }
+  }
+
+  for (const auto &Match :
+       match(cxxRecordDecl(forEach(indirectFieldDecl().bind("indirect"))), *Rec,
+             Ctx)) {
+    const auto *IField = Match.getNodeAs<IndirectFieldDecl>("indirect");
+
+    size_t NumInitializations = false;
+    for (const NamedDecl *ND : IField->chain())
+      NumInitializations += Indirects.erase(llvm::dyn_cast<FieldDecl>(ND));
+
+    if (NumInitializations != 1)
+      return false;
+
+    for (const NamedDecl *ND : IField->chain())
+      Fields.erase(llvm::dyn_cast<FieldDecl>(ND));
+  }
+
+  if (!Fields.empty())
+    return false;
+
+  return true;
+}
+
+const Type *unwrapPointee(const Type *T) {
+  if (!T->isPointerOrReferenceType())
+    return T;
+
+  while (T && T->isPointerOrReferenceType()) {
+    if (T->isReferenceType()) {
+      const QualType QType = T->getPointeeType();
+      if (!QType.isNull())
+        T = QType.getTypePtr();
+    } else
+      T = T->getPointeeOrArrayElementType();
+  }
+
+  return T;
+}
+
+bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                   const bool ConservativeLiteralType);
+
+bool isLiteralType(const Type *T, const ASTContext &Ctx,
+                   const bool ConservativeLiteralType) {
+  if (!T)
+    return false;
+
+  if (!T->isLiteralType(Ctx))
+    return false;
+
+  if (!ConservativeLiteralType)
+    return T->isLiteralType(Ctx) && !T->isVoidType();
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  T = unwrapPointee(T);
+  if (!T)
+    return false;
+
+  assert(!T->isPointerOrReferenceType());
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  if (T->isLiteralType(Ctx))
+    return true;
+
+  if (const auto *Rec = T->getAsCXXRecordDecl()) {
+    if (llvm::any_of(Rec->ctors(), [](const CXXConstructorDecl *Ctor) {
+          return !Ctor->isCopyOrMoveConstructor() &&
+                 Ctor->isConstexprSpecified();
+        }))
+      return false;
+
+    for (const CXXBaseSpecifier Base : Rec->bases()) {
+      if (!isLiteralType(Base.getType(), Ctx, ConservativeLiteralType))
+        return false;
+    }
+  }
+
+  if (const Type *ArrayElementType = T->getArrayElementTypeNoTypeQual())
+    return isLiteralType(ArrayElementType, Ctx, ConservativeLiteralType);
+
+  return false;
+}
+
+bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                   const bool ConservativeLiteralType) {
+  return isLiteralType(QT.getTypePtr(), Ctx, ConservativeLiteralType);
+}
+
+bool satisfiesProperties11(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified()) {
+    return true;
+  }
+  const LangOptions LO = Ctx.getLangOpts();
+  const CXXMethodDecl *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (Method &&
+      (Method->isVirtual() ||
+       !match(cxxMethodDecl(hasBody(cxxTryStmt())), *Method, Ctx).empty()))
+    return false;
+
+  if (const auto *Ctor = llvm::dyn_cast<CXXConstructorDecl>(FDecl);
+      Ctor && (!satisfiesConstructorPropertiesUntil20(Ctor, Ctx) ||
+               llvm::any_of(Ctor->getParent()->bases(),
+                            [](const CXXBaseSpecifier &Base) {
+                              return Base.isVirtual();
+                            })))
+    return false;
+
+  if (const auto *Dtor = llvm::dyn_cast<CXXDestructorDecl>(FDecl);
+      Dtor && !Dtor->isTrivial())
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor11 : public clang::RecursiveASTVisitor<Visitor11> {
+  public:
+    using Base = clang::RecursiveASTVisitor<Visitor11>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor11(ASTContext &Ctx, bool ConservativeLiteralType)
+        : Ctx(Ctx), ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool WalkUpFromNullStmt(NullStmt *) {
+      Possible = false;
+      return true;
+    }
+    bool WalkUpFromDeclStmt(DeclStmt *DS) {
+      for (const Decl *D : DS->decls())
+        if (!llvm::isa<StaticAssertDecl, TypedefNameDecl, UsingDecl,
+                       UsingDirectiveDecl>(D)) {
+          Possible = false;
+          return false;
+        }
+      return true;
+    }
+
+    bool WalkUpFromExpr(Expr *) { return true; }
+    bool WalkUpFromCompoundStmt(CompoundStmt *S) {
+      for (const DynTypedNode &Node : Ctx.getParents(*S))
+        if (Node.get<FunctionDecl>() != nullptr)
+          return true;
+
+      Possible = false;
+      return false;
+    }
+    bool WalkUpFromStmt(Stmt *) {
+      Possible = false;
+      return false;
+    }
+
+    bool WalkUpFromReturnStmt(ReturnStmt *) {
+      ++NumReturns;
+      if (NumReturns != 1U) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool WalkUpFromCastExpr(CastExpr *CE) {
+      if (llvm::is_contained(
+              {
+                  CK_LValueBitCast,
+                  CK_IntegralToPointer,
+                  CK_PointerToIntegral,
+              },
+              CE->getCastKind())) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXDynamicCastExpr(CXXDynamicCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseCXXReinterpretCastExpr(CXXReinterpretCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseType(QualType QT) {
+      if (QT.isNull())
+        return true;
+      if (!isLiteralType(QT, Ctx, ConservativeLiteralType)) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseType(QT);
+    }
+
+    bool WalkUpFromCXXConstructExpr(CXXConstructExpr *CE) {
+      if (const auto *Ctor = CE->getConstructor();
+          Ctor && !Ctor->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+
+      return true;
+    }
+    bool WalkUpFromCallExpr(CallExpr *CE) {
+      if (const auto *FDecl =
+              llvm::dyn_cast_if_present<FunctionDecl>(CE->getCalleeDecl());
+          FDecl && !FDecl->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    bool Possible = true;
+    size_t NumReturns = 0;
+  };
+
+  Visitor11 V{Ctx, ConservativeLiteralType};
+  V.TraverseDecl(const_cast<FunctionDecl *>(FDecl));
+  if (!V.Possible)
+    return false;
+
+  return true;
+}
+
+// The only difference between C++14 and C++17 is that `constexpr` lambdas
+// can be used in C++17.
+bool satisfiesProperties1417(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified())
+    return true;
+
+  const LangOptions LO = Ctx.getLangOpts();
+  const CXXMethodDecl *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (Method && Method->isVirtual())
+    return false;
+
+  if (llvm::isa<CXXConstructorDecl>(FDecl) &&
+      llvm::any_of(
+          Method->getParent()->bases(),
+          [](const CXXBaseSpecifier &Base) { return Base.isVirtual(); }))
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor14 : public clang::RecursiveASTVisitor<Visitor14> {
+  public:
+    using Base = clang::RecursiveASTVisitor<Visitor14>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor14(bool CXX17, ASTContext &Ctx, bool ConservativeLiteralType,
+              bool AddConstexprToMethodOfClassWithoutConstexprConstructor)
+        : CXX17(CXX17), Ctx(Ctx),
+          ConservativeLiteralType(ConservativeLiteralType),
+          AddConstexprToMethodOfClassWithoutConstexprConstructor(
+              AddConstexprToMethodOfClassWithoutConstexprConstructor) {}
+
+    bool TraverseGotoStmt(GotoStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseLabelStmt(LabelStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseCXXTryStmt(CXXTryStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseGCCAsmStmt(GCCAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseMSAsmStmt(MSAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseDecompositionDecl(DecompositionDecl * /*DD*/) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseVarDecl(VarDecl *VD) {
+      const auto StorageDur = VD->getStorageDuration();
+      Possible = VD->hasInit() &&
+                 isLiteralType(VD->getType(), VD->getASTContext(),
+                               ConservativeLiteralType) &&
+                 (StorageDur != StorageDuration::SD_Static &&
+                  StorageDur != StorageDuration::SD_Thread);
+      return Possible && Base::TraverseVarDecl(VD);
+    }
+    bool TraverseLambdaExpr(LambdaExpr *LE) {
+      if (CXX17) {
+        Possible = satisfiesProperties1417(
+            LE->getCallOperator(), Ctx, ConservativeLiteralType,
+            AddConstexprToMethodOfClassWithoutConstexprConstructor);
+        return Possible;
+      }
+      Possible = false;
+      return false;
+    }
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseDeclRefExpr(DeclRefExpr *DRef) {
+      if (const auto *D = llvm::dyn_cast_if_present<VarDecl>(DRef->getDecl());
+          D && !D->isLocalVarDeclOrParm() && D->hasGlobalStorage()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool WalkUpFromCastExpr(CastExpr *CE) {
+      if (llvm::is_contained(
+              {
+                  CK_LValueBitCast,
+                  CK_IntegralToPointer,
+                  CK_PointerToIntegral,
+              },
+              CE->getCastKind())) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXDynamicCastExpr(CXXDynamicCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseCXXReinterpretCastExpr(CXXReinterpretCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    const bool CXX17;
+    bool Possible = true;
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor;
+  };
+
+  Visitor14 V{Ctx.getLangOpts().CPlusPlus17 != 0, Ctx, ConservativeLiteralType,
+              AddConstexprToMethodOfClassWithoutConstexprConstructor};
+  V.TraverseDecl(const_cast<FunctionDecl *>(FDecl));
+  if (!V.Possible)
+    return false;
+
+  if (const auto *Ctor = llvm::dyn_cast<CXXConstructorDecl>(FDecl);
+      Ctor && !satisfiesConstructorPropertiesUntil20(Ctor, Ctx))
+    return false;
+
+  if (const auto *Dtor = llvm::dyn_cast<CXXDestructorDecl>(FDecl);
+      Dtor && !Dtor->isTrivial())
+    return false;
+
+  class BodyVisitor : public clang::RecursiveASTVisitor<BodyVisitor> {
+  public:
+    using Base = clang::RecursiveASTVisitor<BodyVisitor>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    explicit BodyVisitor(ASTContext &Ctx, bool ConservativeLiteralType)
+        : Ctx(Ctx), ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool TraverseType(QualType QT) {
+      if (QT.isNull())
+        return true;
+      if (!isLiteralType(QT, Ctx, ConservativeLiteralType)) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseType(QT);
+    }
+
+    bool WalkUpFromCXXConstructExpr(CXXConstructExpr *CE) {
+      if (const auto *Ctor = CE->getConstructor();
+          Ctor && !Ctor->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+
+      return true;
+    }
+    bool WalkUpFromCallExpr(CallExpr *CE) {
+      if (const auto *FDecl =
+              llvm::dyn_cast_if_present<FunctionDecl>(CE->getCalleeDecl());
+          FDecl && !FDecl->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    bool Possible = true;
+  };
+
+  if (FDecl->hasBody() && ConservativeLiteralType) {
+    BodyVisitor Visitor(Ctx, ConservativeLiteralType);
+    Visitor.TraverseStmt(FDecl->getBody());
+    if (!Visitor.Possible)
+      return false;
+  }
+  return true;
+}
+
+bool satisfiesProperties20(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified()) {
+    return true;
+  }
+  const LangOptions LO = Ctx.getLangOpts();
+  const CXXMethodDecl *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (FDecl->hasBody() && llvm::isa<CoroutineBodyStmt>(FDecl->getBody()))
+    return false;
+
+  if ((llvm::isa<CXXConstructorDecl>(FDecl) ||
+       llvm::isa<CXXDestructorDecl>(FDecl)) &&
+      llvm::any_of(
+          Method->getParent()->bases(),
+          [](const CXXBaseSpecifier &Base) { return Base.isVirtual(); }))
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor20 : public clang::RecursiveASTVisitor<Visitor20> {
+  public:
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor20(bool ConservativeLiteralType)
+        : ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool TraverseGotoStmt(GotoStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseLabelStmt(LabelStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseCXXTryStmt(CXXTryStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseGCCAsmStmt(GCCAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseMSAsmStmt(MSAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseDecompositionDecl(DecompositionDecl * /*DD*/) {
+      Possible = f...
[truncated]

@llvmbot
Copy link
Member

llvmbot commented Jul 1, 2025

@llvm/pr-subscribers-clang-tools-extra

Author: Julian Schmidt (5chmidti)

Changes

This check finds all functions and variables that can be declared as
constexpr, using the specified standard version to check if the
requirements are met.

Fixes #115622


Patch is 95.12 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/146553.diff

9 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/misc/CMakeLists.txt (+1)
  • (added) clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp (+936)
  • (added) clang-tools-extra/clang-tidy/misc/ConstexprCheck.h (+43)
  • (modified) clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp (+3)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/list.rst (+1)
  • (added) clang-tools-extra/docs/clang-tidy/checks/misc/constexpr.rst (+40)
  • (added) clang-tools-extra/test/clang-tidy/checkers/misc/constexpr-cxx20.cpp (+36)
  • (added) clang-tools-extra/test/clang-tidy/checkers/misc/constexpr-ref.cpp (+383)
  • (added) clang-tools-extra/test/clang-tidy/checkers/misc/constexpr.cpp (+562)
diff --git a/clang-tools-extra/clang-tidy/misc/CMakeLists.txt b/clang-tools-extra/clang-tidy/misc/CMakeLists.txt
index fd7affd22a463..4535df3451c95 100644
--- a/clang-tools-extra/clang-tidy/misc/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/misc/CMakeLists.txt
@@ -19,6 +19,7 @@ set_target_properties(genconfusable PROPERTIES FOLDER "Clang Tools Extra/Sourceg
 
 add_clang_library(clangTidyMiscModule STATIC
   ConstCorrectnessCheck.cpp
+  ConstexprCheck.cpp
   CoroutineHostileRAIICheck.cpp
   DefinitionsInHeadersCheck.cpp
   ConfusableIdentifierCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp b/clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp
new file mode 100644
index 0000000000000..270ac1dd0fa48
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/misc/ConstexprCheck.cpp
@@ -0,0 +1,936 @@
+//===--- ConstexprCheck.cpp - clang-tidy ----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "ConstexprCheck.h"
+#include "../utils/ASTUtils.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclBase.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/OperationKinds.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/AST/Stmt.h"
+#include "clang/AST/StmtCXX.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/ASTMatchers/ASTMatchersInternal.h"
+#include "clang/ASTMatchers/ASTMatchersMacros.h"
+#include "clang/Basic/LangOptions.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Basic/Specifiers.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Support/Casting.h"
+#include <cstddef>
+#include <functional>
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::misc {
+
+namespace {
+AST_MATCHER(FunctionDecl, locationPermitsConstexpr) {
+  const bool IsInMainFile =
+      Finder->getASTContext().getSourceManager().isInMainFile(
+          Node.getLocation());
+
+  if (IsInMainFile && Node.hasExternalFormalLinkage())
+    return false;
+  if (!IsInMainFile && !Node.isInlined())
+    return false;
+
+  return true;
+}
+
+AST_MATCHER(Expr, isCXX11ConstantExpr) {
+  return !Node.isValueDependent() &&
+         Node.isCXX11ConstantExpr(Finder->getASTContext());
+}
+
+AST_MATCHER(DeclaratorDecl, isInMacro) {
+  const SourceRange R =
+      SourceRange(Node.getInnerLocStart(), Node.getLocation());
+
+  return Node.getLocation().isMacroID() || Node.getEndLoc().isMacroID() ||
+         utils::rangeContainsMacroExpansion(
+             R, &Finder->getASTContext().getSourceManager()) ||
+         utils::rangeIsEntirelyWithinMacroArgument(
+             R, &Finder->getASTContext().getSourceManager());
+}
+
+AST_MATCHER(Decl, hasNoRedecl) {
+  // There is always the actual declaration
+  return !Node.redecls().empty() &&
+         std::next(Node.redecls_begin()) == Node.redecls_end();
+}
+
+AST_MATCHER(Decl, allRedeclsInSameFile) {
+  const SourceManager &SM = Finder->getASTContext().getSourceManager();
+  const SourceLocation L = Node.getLocation();
+  for (const Decl *ReDecl : Node.redecls()) {
+    if (!SM.isWrittenInSameFile(L, ReDecl->getLocation()))
+      return false;
+  }
+  return true;
+}
+
+AST_MATCHER(FunctionDecl, isConstexprSpecified) {
+  return Node.isConstexprSpecified();
+}
+
+bool satisfiesConstructorPropertiesUntil20(const CXXConstructorDecl *Ctor,
+                                           ASTContext &Ctx) {
+  const CXXRecordDecl *Rec = Ctor->getParent();
+  llvm::SmallPtrSet<const RecordDecl *, 8> Bases{};
+  for (const CXXBaseSpecifier Base : Rec->bases()) {
+    Bases.insert(Base.getType()->getAsRecordDecl());
+  }
+  llvm::SmallPtrSet<const FieldDecl *, 8> Fields{Rec->field_begin(),
+                                                 Rec->field_end()};
+  llvm::SmallPtrSet<const FieldDecl *, 4> Indirects{};
+
+  for (const CXXCtorInitializer *const Init : Ctor->inits()) {
+    const Type *InitType = Init->getBaseClass();
+    if (InitType && InitType->isRecordType()) {
+      const auto *ConstructingInit =
+          llvm::dyn_cast<CXXConstructExpr>(Init->getInit());
+      if (ConstructingInit &&
+          !ConstructingInit->getConstructor()->isConstexprSpecified())
+        return false;
+    }
+
+    if (Init->isBaseInitializer()) {
+      Bases.erase(Init->getBaseClass()->getAsRecordDecl());
+      continue;
+    }
+
+    if (Init->isMemberInitializer()) {
+      const FieldDecl *Field = Init->getMember();
+
+      if (Field->isAnonymousStructOrUnion())
+        Indirects.insert(Field);
+
+      Fields.erase(Field);
+      continue;
+    }
+  }
+
+  for (const auto &Match :
+       match(cxxRecordDecl(forEach(indirectFieldDecl().bind("indirect"))), *Rec,
+             Ctx)) {
+    const auto *IField = Match.getNodeAs<IndirectFieldDecl>("indirect");
+
+    size_t NumInitializations = false;
+    for (const NamedDecl *ND : IField->chain())
+      NumInitializations += Indirects.erase(llvm::dyn_cast<FieldDecl>(ND));
+
+    if (NumInitializations != 1)
+      return false;
+
+    for (const NamedDecl *ND : IField->chain())
+      Fields.erase(llvm::dyn_cast<FieldDecl>(ND));
+  }
+
+  if (!Fields.empty())
+    return false;
+
+  return true;
+}
+
+const Type *unwrapPointee(const Type *T) {
+  if (!T->isPointerOrReferenceType())
+    return T;
+
+  while (T && T->isPointerOrReferenceType()) {
+    if (T->isReferenceType()) {
+      const QualType QType = T->getPointeeType();
+      if (!QType.isNull())
+        T = QType.getTypePtr();
+    } else
+      T = T->getPointeeOrArrayElementType();
+  }
+
+  return T;
+}
+
+bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                   const bool ConservativeLiteralType);
+
+bool isLiteralType(const Type *T, const ASTContext &Ctx,
+                   const bool ConservativeLiteralType) {
+  if (!T)
+    return false;
+
+  if (!T->isLiteralType(Ctx))
+    return false;
+
+  if (!ConservativeLiteralType)
+    return T->isLiteralType(Ctx) && !T->isVoidType();
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  T = unwrapPointee(T);
+  if (!T)
+    return false;
+
+  assert(!T->isPointerOrReferenceType());
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  if (T->isLiteralType(Ctx))
+    return true;
+
+  if (const auto *Rec = T->getAsCXXRecordDecl()) {
+    if (llvm::any_of(Rec->ctors(), [](const CXXConstructorDecl *Ctor) {
+          return !Ctor->isCopyOrMoveConstructor() &&
+                 Ctor->isConstexprSpecified();
+        }))
+      return false;
+
+    for (const CXXBaseSpecifier Base : Rec->bases()) {
+      if (!isLiteralType(Base.getType(), Ctx, ConservativeLiteralType))
+        return false;
+    }
+  }
+
+  if (const Type *ArrayElementType = T->getArrayElementTypeNoTypeQual())
+    return isLiteralType(ArrayElementType, Ctx, ConservativeLiteralType);
+
+  return false;
+}
+
+bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                   const bool ConservativeLiteralType) {
+  return isLiteralType(QT.getTypePtr(), Ctx, ConservativeLiteralType);
+}
+
+bool satisfiesProperties11(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified()) {
+    return true;
+  }
+  const LangOptions LO = Ctx.getLangOpts();
+  const CXXMethodDecl *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (Method &&
+      (Method->isVirtual() ||
+       !match(cxxMethodDecl(hasBody(cxxTryStmt())), *Method, Ctx).empty()))
+    return false;
+
+  if (const auto *Ctor = llvm::dyn_cast<CXXConstructorDecl>(FDecl);
+      Ctor && (!satisfiesConstructorPropertiesUntil20(Ctor, Ctx) ||
+               llvm::any_of(Ctor->getParent()->bases(),
+                            [](const CXXBaseSpecifier &Base) {
+                              return Base.isVirtual();
+                            })))
+    return false;
+
+  if (const auto *Dtor = llvm::dyn_cast<CXXDestructorDecl>(FDecl);
+      Dtor && !Dtor->isTrivial())
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor11 : public clang::RecursiveASTVisitor<Visitor11> {
+  public:
+    using Base = clang::RecursiveASTVisitor<Visitor11>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor11(ASTContext &Ctx, bool ConservativeLiteralType)
+        : Ctx(Ctx), ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool WalkUpFromNullStmt(NullStmt *) {
+      Possible = false;
+      return true;
+    }
+    bool WalkUpFromDeclStmt(DeclStmt *DS) {
+      for (const Decl *D : DS->decls())
+        if (!llvm::isa<StaticAssertDecl, TypedefNameDecl, UsingDecl,
+                       UsingDirectiveDecl>(D)) {
+          Possible = false;
+          return false;
+        }
+      return true;
+    }
+
+    bool WalkUpFromExpr(Expr *) { return true; }
+    bool WalkUpFromCompoundStmt(CompoundStmt *S) {
+      for (const DynTypedNode &Node : Ctx.getParents(*S))
+        if (Node.get<FunctionDecl>() != nullptr)
+          return true;
+
+      Possible = false;
+      return false;
+    }
+    bool WalkUpFromStmt(Stmt *) {
+      Possible = false;
+      return false;
+    }
+
+    bool WalkUpFromReturnStmt(ReturnStmt *) {
+      ++NumReturns;
+      if (NumReturns != 1U) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool WalkUpFromCastExpr(CastExpr *CE) {
+      if (llvm::is_contained(
+              {
+                  CK_LValueBitCast,
+                  CK_IntegralToPointer,
+                  CK_PointerToIntegral,
+              },
+              CE->getCastKind())) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXDynamicCastExpr(CXXDynamicCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseCXXReinterpretCastExpr(CXXReinterpretCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseType(QualType QT) {
+      if (QT.isNull())
+        return true;
+      if (!isLiteralType(QT, Ctx, ConservativeLiteralType)) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseType(QT);
+    }
+
+    bool WalkUpFromCXXConstructExpr(CXXConstructExpr *CE) {
+      if (const auto *Ctor = CE->getConstructor();
+          Ctor && !Ctor->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+
+      return true;
+    }
+    bool WalkUpFromCallExpr(CallExpr *CE) {
+      if (const auto *FDecl =
+              llvm::dyn_cast_if_present<FunctionDecl>(CE->getCalleeDecl());
+          FDecl && !FDecl->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    bool Possible = true;
+    size_t NumReturns = 0;
+  };
+
+  Visitor11 V{Ctx, ConservativeLiteralType};
+  V.TraverseDecl(const_cast<FunctionDecl *>(FDecl));
+  if (!V.Possible)
+    return false;
+
+  return true;
+}
+
+// The only difference between C++14 and C++17 is that `constexpr` lambdas
+// can be used in C++17.
+bool satisfiesProperties1417(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified())
+    return true;
+
+  const LangOptions LO = Ctx.getLangOpts();
+  const CXXMethodDecl *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (Method && Method->isVirtual())
+    return false;
+
+  if (llvm::isa<CXXConstructorDecl>(FDecl) &&
+      llvm::any_of(
+          Method->getParent()->bases(),
+          [](const CXXBaseSpecifier &Base) { return Base.isVirtual(); }))
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor14 : public clang::RecursiveASTVisitor<Visitor14> {
+  public:
+    using Base = clang::RecursiveASTVisitor<Visitor14>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor14(bool CXX17, ASTContext &Ctx, bool ConservativeLiteralType,
+              bool AddConstexprToMethodOfClassWithoutConstexprConstructor)
+        : CXX17(CXX17), Ctx(Ctx),
+          ConservativeLiteralType(ConservativeLiteralType),
+          AddConstexprToMethodOfClassWithoutConstexprConstructor(
+              AddConstexprToMethodOfClassWithoutConstexprConstructor) {}
+
+    bool TraverseGotoStmt(GotoStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseLabelStmt(LabelStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseCXXTryStmt(CXXTryStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseGCCAsmStmt(GCCAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseMSAsmStmt(MSAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseDecompositionDecl(DecompositionDecl * /*DD*/) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseVarDecl(VarDecl *VD) {
+      const auto StorageDur = VD->getStorageDuration();
+      Possible = VD->hasInit() &&
+                 isLiteralType(VD->getType(), VD->getASTContext(),
+                               ConservativeLiteralType) &&
+                 (StorageDur != StorageDuration::SD_Static &&
+                  StorageDur != StorageDuration::SD_Thread);
+      return Possible && Base::TraverseVarDecl(VD);
+    }
+    bool TraverseLambdaExpr(LambdaExpr *LE) {
+      if (CXX17) {
+        Possible = satisfiesProperties1417(
+            LE->getCallOperator(), Ctx, ConservativeLiteralType,
+            AddConstexprToMethodOfClassWithoutConstexprConstructor);
+        return Possible;
+      }
+      Possible = false;
+      return false;
+    }
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseDeclRefExpr(DeclRefExpr *DRef) {
+      if (const auto *D = llvm::dyn_cast_if_present<VarDecl>(DRef->getDecl());
+          D && !D->isLocalVarDeclOrParm() && D->hasGlobalStorage()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool WalkUpFromCastExpr(CastExpr *CE) {
+      if (llvm::is_contained(
+              {
+                  CK_LValueBitCast,
+                  CK_IntegralToPointer,
+                  CK_PointerToIntegral,
+              },
+              CE->getCastKind())) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXDynamicCastExpr(CXXDynamicCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseCXXReinterpretCastExpr(CXXReinterpretCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    const bool CXX17;
+    bool Possible = true;
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor;
+  };
+
+  Visitor14 V{Ctx.getLangOpts().CPlusPlus17 != 0, Ctx, ConservativeLiteralType,
+              AddConstexprToMethodOfClassWithoutConstexprConstructor};
+  V.TraverseDecl(const_cast<FunctionDecl *>(FDecl));
+  if (!V.Possible)
+    return false;
+
+  if (const auto *Ctor = llvm::dyn_cast<CXXConstructorDecl>(FDecl);
+      Ctor && !satisfiesConstructorPropertiesUntil20(Ctor, Ctx))
+    return false;
+
+  if (const auto *Dtor = llvm::dyn_cast<CXXDestructorDecl>(FDecl);
+      Dtor && !Dtor->isTrivial())
+    return false;
+
+  class BodyVisitor : public clang::RecursiveASTVisitor<BodyVisitor> {
+  public:
+    using Base = clang::RecursiveASTVisitor<BodyVisitor>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    explicit BodyVisitor(ASTContext &Ctx, bool ConservativeLiteralType)
+        : Ctx(Ctx), ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool TraverseType(QualType QT) {
+      if (QT.isNull())
+        return true;
+      if (!isLiteralType(QT, Ctx, ConservativeLiteralType)) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseType(QT);
+    }
+
+    bool WalkUpFromCXXConstructExpr(CXXConstructExpr *CE) {
+      if (const auto *Ctor = CE->getConstructor();
+          Ctor && !Ctor->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+
+      return true;
+    }
+    bool WalkUpFromCallExpr(CallExpr *CE) {
+      if (const auto *FDecl =
+              llvm::dyn_cast_if_present<FunctionDecl>(CE->getCalleeDecl());
+          FDecl && !FDecl->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    bool Possible = true;
+  };
+
+  if (FDecl->hasBody() && ConservativeLiteralType) {
+    BodyVisitor Visitor(Ctx, ConservativeLiteralType);
+    Visitor.TraverseStmt(FDecl->getBody());
+    if (!Visitor.Possible)
+      return false;
+  }
+  return true;
+}
+
+bool satisfiesProperties20(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified()) {
+    return true;
+  }
+  const LangOptions LO = Ctx.getLangOpts();
+  const CXXMethodDecl *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (FDecl->hasBody() && llvm::isa<CoroutineBodyStmt>(FDecl->getBody()))
+    return false;
+
+  if ((llvm::isa<CXXConstructorDecl>(FDecl) ||
+       llvm::isa<CXXDestructorDecl>(FDecl)) &&
+      llvm::any_of(
+          Method->getParent()->bases(),
+          [](const CXXBaseSpecifier &Base) { return Base.isVirtual(); }))
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor20 : public clang::RecursiveASTVisitor<Visitor20> {
+  public:
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor20(bool ConservativeLiteralType)
+        : ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool TraverseGotoStmt(GotoStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseLabelStmt(LabelStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseCXXTryStmt(CXXTryStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseGCCAsmStmt(GCCAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseMSAsmStmt(MSAsmStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool TraverseDecompositionDecl(DecompositionDecl * /*DD*/) {
+      Possible = f...
[truncated]

Copy link

github-actions bot commented Jul 1, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@5chmidti 5chmidti force-pushed the clang_tidy_constexpr branch from 35a081f to 2a50c3e Compare July 1, 2025 15:55
@carlosgalvezp
Copy link
Contributor

misc-use-constexpr? :)

@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 1, 2025

I ran this check on all of clang/ and clang-tools-extra/ and got no compilation errors from too aggressively changing things to constexpr.

Some perf info from running the checks enabled in LLVM + this check:

Running on: clang/lib/Sema/SemaExprCXX.cpp (1 diag):

===-------------------------------------------------------------------------===
                          clang-tidy checks profiling
===-------------------------------------------------------------------------===
  Total Execution Time: 6.4289 seconds (6.4453 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
   0.7024 ( 20.6%)   0.7401 ( 24.5%)   1.4424 ( 22.4%)   1.4457 ( 22.4%)  misc-unused-using-decls
   0.6188 ( 18.2%)   0.1852 (  6.1%)   0.8040 ( 12.5%)   0.8069 ( 12.5%)  llvm-qualified-auto
   0.2833 (  8.3%)   0.2724 (  9.0%)   0.5556 (  8.6%)   0.5581 (  8.7%)  llvm-prefer-isa-or-dyn-cast-in-conditionals
   0.2288 (  6.7%)   0.2426 (  8.0%)   0.4714 (  7.3%)   0.4715 (  7.3%)  misc-misleading-identifier
   0.2271 (  6.7%)   0.2140 (  7.1%)   0.4411 (  6.9%)   0.4423 (  6.9%)  misc-confusable-identifiers
   0.2132 (  6.3%)   0.2268 (  7.5%)   0.4400 (  6.8%)   0.4402 (  6.8%)  misc-definitions-in-headers
   0.1938 (  5.7%)   0.2066 (  6.8%)   0.4003 (  6.2%)   0.4008 (  6.2%)  misc-non-copyable-objects
   0.1371 (  4.0%)   0.1386 (  4.6%)   0.2757 (  4.3%)   0.2750 (  4.3%)  misc-unconventional-assign-operator
   0.1235 (  3.6%)   0.1296 (  4.3%)   0.2531 (  3.9%)   0.2549 (  4.0%)  misc-constexpr
   0.1146 (  3.4%)   0.1188 (  3.9%)   0.2334 (  3.6%)   0.2346 (  3.6%)  misc-misplaced-const
   0.1176 (  3.5%)   0.1061 (  3.5%)   0.2238 (  3.5%)   0.2236 (  3.5%)  misc-redundant-expression
   0.1074 (  3.2%)   0.1110 (  3.7%)   0.2184 (  3.4%)   0.2208 (  3.4%)  misc-use-internal-linkage
   0.0665 (  2.0%)   0.0720 (  2.4%)   0.1385 (  2.2%)   0.1400 (  2.2%)  misc-new-delete-overloads
   0.0615 (  1.8%)   0.0640 (  2.1%)   0.1255 (  2.0%)   0.1257 (  2.0%)  llvm-prefer-register-over-unsigned
   0.0548 (  1.6%)   0.0572 (  1.9%)   0.1120 (  1.7%)   0.1121 (  1.7%)  llvm-twine-local
   0.0444 (  1.3%)   0.0328 (  1.1%)   0.0771 (  1.2%)   0.0773 (  1.2%)  llvm-namespace-comment
   0.0385 (  1.1%)   0.0312 (  1.0%)   0.0697 (  1.1%)   0.0695 (  1.1%)  llvm-else-after-return
   0.0325 (  1.0%)   0.0351 (  1.2%)   0.0677 (  1.1%)   0.0674 (  1.0%)  misc-unused-alias-decls
   0.0294 (  0.9%)   0.0255 (  0.8%)   0.0549 (  0.9%)   0.0547 (  0.8%)  misc-static-assert
   0.0108 (  0.3%)   0.0100 (  0.3%)   0.0208 (  0.3%)   0.0207 (  0.3%)  misc-uniqueptr-reset-release
   0.0018 (  0.1%)   0.0018 (  0.1%)   0.0036 (  0.1%)   0.0035 (  0.1%)  misc-misleading-bidirectional
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)  misc-throw-by-value-catch-by-reference
   3.4077 (100.0%)   3.0212 (100.0%)   6.4289 (100.0%)   6.4453 (100.0%)  Total

Running on clang/utils/TableGen/RISCVVEmitter.cpp (3 diags):

===-------------------------------------------------------------------------===
                          clang-tidy checks profiling
===-------------------------------------------------------------------------===
  Total Execution Time: 1.1712 seconds (1.1748 wall clock)

   ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
   0.1293 ( 20.2%)   0.1265 ( 23.8%)   0.2557 ( 21.8%)   0.2569 ( 21.9%)  misc-unused-using-decls
   0.0673 ( 10.5%)   0.0576 ( 10.9%)   0.1249 ( 10.7%)   0.1257 ( 10.7%)  llvm-prefer-isa-or-dyn-cast-in-conditionals
   0.0833 ( 13.0%)   0.0275 (  5.2%)   0.1108 (  9.5%)   0.1112 (  9.5%)  llvm-qualified-auto
   0.0434 (  6.8%)   0.0407 (  7.7%)   0.0840 (  7.2%)   0.0843 (  7.2%)  misc-misleading-identifier
   0.0436 (  6.8%)   0.0385 (  7.2%)   0.0820 (  7.0%)   0.0817 (  7.0%)  misc-confusable-identifiers
   0.0417 (  6.5%)   0.0387 (  7.3%)   0.0804 (  6.9%)   0.0806 (  6.9%)  misc-definitions-in-headers
   0.0383 (  6.0%)   0.0359 (  6.8%)   0.0742 (  6.3%)   0.0744 (  6.3%)  misc-non-copyable-objects
   0.0275 (  4.3%)   0.0217 (  4.1%)   0.0492 (  4.2%)   0.0490 (  4.2%)  misc-redundant-expression
   0.0229 (  3.6%)   0.0215 (  4.1%)   0.0445 (  3.8%)   0.0443 (  3.8%)  misc-constexpr
   0.0229 (  3.6%)   0.0203 (  3.8%)   0.0432 (  3.7%)   0.0431 (  3.7%)  misc-unconventional-assign-operator
   0.0200 (  3.1%)   0.0191 (  3.6%)   0.0391 (  3.3%)   0.0391 (  3.3%)  misc-misplaced-const
   0.0199 (  3.1%)   0.0185 (  3.5%)   0.0384 (  3.3%)   0.0390 (  3.3%)  misc-use-internal-linkage
   0.0154 (  2.4%)   0.0081 (  1.5%)   0.0235 (  2.0%)   0.0236 (  2.0%)  llvm-namespace-comment
   0.0114 (  1.8%)   0.0111 (  2.1%)   0.0225 (  1.9%)   0.0229 (  1.9%)  llvm-prefer-register-over-unsigned
   0.0115 (  1.8%)   0.0104 (  2.0%)   0.0219 (  1.9%)   0.0222 (  1.9%)  misc-new-delete-overloads
   0.0104 (  1.6%)   0.0102 (  1.9%)   0.0206 (  1.8%)   0.0207 (  1.8%)  llvm-twine-local
   0.0122 (  1.9%)   0.0083 (  1.6%)   0.0205 (  1.8%)   0.0205 (  1.7%)  misc-static-assert
   0.0108 (  1.7%)   0.0074 (  1.4%)   0.0182 (  1.6%)   0.0181 (  1.5%)  llvm-else-after-return
   0.0069 (  1.1%)   0.0067 (  1.3%)   0.0135 (  1.2%)   0.0135 (  1.2%)  misc-unused-alias-decls
   0.0013 (  0.2%)   0.0012 (  0.2%)   0.0025 (  0.2%)   0.0025 (  0.2%)  misc-uniqueptr-reset-release
   0.0006 (  0.1%)   0.0008 (  0.1%)   0.0014 (  0.1%)   0.0014 (  0.1%)  misc-misleading-bidirectional
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.0%)  misc-throw-by-value-catch-by-reference
   0.6406 (100.0%)   0.5306 (100.0%)   1.1712 (100.0%)   1.1748 (100.0%)  Total

@denzor200
Copy link

misc-use-constexpr? :)

modernize-use-constexpr?

@5chmidti 5chmidti force-pushed the clang_tidy_constexpr branch from 2a50c3e to 8d35424 Compare July 1, 2025 16:11
This check finds all functions and variables that can be declared as
`constexpr`, using the specified standard version to check if the
requirements are met.

Fixes llvm#115622
@5chmidti 5chmidti force-pushed the clang_tidy_constexpr branch from 8d35424 to 16d5a3f Compare July 1, 2025 16:18
@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 1, 2025

misc-use-constexpr? :)

modernize-use-constexpr?

Hm. It is indeed also a modernization... +- on which to pick. Let's see what others think.
On one hand, the modernize checks have

  • modernize-use-auto
  • modernize-use-nodiscard
  • modernize-use-noexcept
  • modernize-use-override
  • modernize-use-using

while the misc category has misc-const-correctness, which is also close to this check. Though, constexpr also provides additional benefits and not just modernization (though that can also be said about some of the other modernize checks). It can provide additional safety checks if UB would be encountered at compile-time, e.g.,

constexpr int div(int a, int b) { return a / b; }
int main() {
  constexpr auto res = div(10,0);
  return 0;
}

, or potential performance improvements.

@denzor200
Copy link

constexpr is one of the important additions of C++11. Most of the users will search this check in "modernize" section and will be misled by not finding it there.
"misc-const-correctness" - not a modernize check at all, it even relevant for C++98, so it exists in "misc"

@vbvictor
Copy link
Contributor

vbvictor commented Jul 1, 2025

I'm +1 on the modernize-use-constexpr.

@carlosgalvezp
Copy link
Contributor

I'm ok with modernize-use-constexpr as well!

@5chmidti 5chmidti changed the title [clang-tidy] add misc-constexpr check [clang-tidy] add modernize-use-constexpr check Jul 1, 2025
@seanm
Copy link

seanm commented Jul 1, 2025

I tried with ITK and the resulting transformation did not compile.

Many changes were of this form:

-    const double     Max = 1.0 - Min;
+    constexpr  double     Max = 1.0 - Min;

Which is great, though notice the double space after constexpr for some reason.

Other changes were like this:

-  const auto check = [](const auto & ptr) { EXPECT_THROW(itk::Deref(ptr), itk::DerefError); };
+  constexpr const auto check = [](auto & ptr) { EXPECT_THROW(itk::Deref(ptr), itk::DerefError); };

I'm no C++ expert, but is it right to have both const and constexpr here?

Also, I was surprised to see the 2nd const removed. And this removal generates one of the many compiler errors.

Still, this is looking like it'll be great!

@schenker
Copy link
Contributor

schenker commented Jul 1, 2025

I got some fixits that do not compile, e.g.:

std::erase_if(numbers, [](int n) {
return n % 2 == 0;
});

is changed into

std::erase_if(numbers, [](int nconstexpr ) {
return n % 2 == 0;
});

@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 1, 2025

I tried with ITK and the resulting transformation did not compile.

Many changes were of this form:

-    const double     Max = 1.0 - Min;
+    constexpr  double     Max = 1.0 - Min;

Which is great, though notice the double space after constexpr for some reason.

The reason is that when inserting, I add a constexpr with a trailing space, but when it is found, also remove the const token without its corresponding space. This will have to be fixed by running clang-format; we have a few of these additional whitespaces in some checks.

Other changes were like this:

-  const auto check = [](const auto & ptr) { EXPECT_THROW(itk::Deref(ptr), itk::DerefError); };
+  constexpr const auto check = [](auto & ptr) { EXPECT_THROW(itk::Deref(ptr), itk::DerefError); };

I'm no C++ expert, but is it right to have both const and constexpr here?

Also, I was surprised to see the 2nd const removed. And this removal generates one of the many compiler errors.

Eh... yeah. The range I passed to the getQualifyingToken function was too big and included more than it needed. It's fixed now, and I've added a test for it.

Still, this is looking like it'll be great!

Thanks, and thank you very much for trying it out. My current setup is limited in what I can test outside the LLVM codebase.

@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 1, 2025

Sorry for the silly mistakes, btw.

I've updated the docs, added replacement string options, and fixed the const keyword removal.

@schenker
Copy link
Contributor

schenker commented Jul 2, 2025

I've tried to reproduce this but was unable to, could you please provide a small reproducer?

With 127d7f0 I can reporoduce the wrong fixit like this:

test.cpp

#include <vector>
template <typename T> void func() {
  std::vector<int> Numbers = {0, 1};
  const auto count =
  std::erase_if(Numbers, [](int N) { return N % 2 == 0; });
}

commands

clang-tidy -checks=-*,*constexpr* -export-fixes fixes.yaml test.cpp -- --std=c++23
clang-apply-replacements .

result

#include <vector>
template <typename T> void func() {
  std::vector<int> Numbers = {0, 1};
  const auto count =
  std::erase_if(Numbers, [](int Nconstexpr ) { return N % 2 == 0; });
}

@seanm
Copy link

seanm commented Jul 2, 2025

Another compiler error after transformation:

It made this constexpr:

	PUGI_IMPL_FN constexpr bool is_nan(double value)
	{
	#if defined(PUGI_IMPL_MSVC_CRT_VERSION) || defined(__BORLANDC__)
		return !!_isnan(value);
	#elif defined(fpclassify) && defined(FP_NAN)
		return fpclassify(value) == FP_NAN;
	#else
		// fallback
		const volatile double v = value;
		return v != v;
	#endif
	}

But then:

/Users/sean/external/VTK/ThirdParty/pugixml/vtkpugixml/src/pugixml.cpp:8413:30: error: constexpr function never produces a constant expression [-Winvalid-constexpr]
 8413 |         PUGI_IMPL_FN constexpr bool is_nan(double value)
      |                                     ^~~~~~
/Users/sean/external/VTK/ThirdParty/pugixml/vtkpugixml/src/pugixml.cpp:8422:15: note: read of volatile-qualified type 'const volatile double' is not allowed in a constant expression
 8422 |                 return v != v;
      |                             ^

@seanm
Copy link

seanm commented Jul 2, 2025

Another interesting case, it seems somehow multiple constexprs got added, ex:

template <typename CellType>
constexpr constexpr constexpr constexpr constexpr constexpr constexpr constexpr int numberOfSidesOfDimension(int dimension);

This actually compiles, though with a -Wduplicate-decl-specifier warning.

@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 2, 2025

Another compiler error after transformation:

It made this constexpr:

	PUGI_IMPL_FN constexpr bool is_nan(double value)
	{
	#if defined(PUGI_IMPL_MSVC_CRT_VERSION) || defined(__BORLANDC__)
		return !!_isnan(value);
	#elif defined(fpclassify) && defined(FP_NAN)
		return fpclassify(value) == FP_NAN;
	#else
		// fallback
		const volatile double v = value;
		return v != v;
	#endif
	}

But then:

/Users/sean/external/VTK/ThirdParty/pugixml/vtkpugixml/src/pugixml.cpp:8413:30: error: constexpr function never produces a constant expression [-Winvalid-constexpr]
 8413 |         PUGI_IMPL_FN constexpr bool is_nan(double value)
      |                                     ^~~~~~
/Users/sean/external/VTK/ThirdParty/pugixml/vtkpugixml/src/pugixml.cpp:8422:15: note: read of volatile-qualified type 'const volatile double' is not allowed in a constant expression
 8422 |                 return v != v;
      |                             ^

I'll take a look

@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 2, 2025

Another interesting case, it seems somehow multiple constexprs got added, ex:

template <typename CellType>
constexpr constexpr constexpr constexpr constexpr constexpr constexpr constexpr int numberOfSidesOfDimension(int dimension);

This actually compiles, though with a -Wduplicate-decl-specifier warning.

I failed to reproduce this with

template <typename T>
static T forwardDeclared();

template <typename T>
static T forwardDeclared() { return T{}; }

static void useForwardDeclared() {
    forwardDeclared<int>() + forwardDeclared<double>() + forwardDeclared<char>();
}
 template <typename T>                                                                                                                                        
-static T forwardDeclared();                                
+constexpr static T forwardDeclared();                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 template <typename T>                                                      
-static T forwardDeclared() { return T{}; }
+constexpr static T forwardDeclared() { return T{}; }                                                                                                                                                                                                                                                                       
                                                                  
 static void useForwardDeclared() {                                                                                                                                                                                                                                                                                         
     forwardDeclared<int>() + forwardDeclared<double>() + forwardDeclared<char>();  

I had seen this one as well, but it was already fixed in the initial commit, at least I thought.

@seanm
Copy link

seanm commented Jul 2, 2025

This actually compiles, though with a -Wduplicate-decl-specifier warning.

I failed to reproduce this...

I don't have a minimal repro case, I was building VTK. It's pretty simple to build it yourself if you're familiar with CMake. I could give you exact command line steps if you need...

@seanm
Copy link

seanm commented Jul 2, 2025

I've tried another project: opencv and the result also fails to compile.

One case I think is because two variables are declared together. i.e.:

int foo, bar;

instead of:

int foo;
int bar;

Concretely:

    const char * const borderMap[] = { "BORDER_CONSTANT", "BORDER_REPLICATE", "BORDER_REFLECT", "BORDER_WRAP", "BORDER_REFLECT_101" },
        * const btype = borderMap[borderType & ~BORDER_ISOLATED];

Got changed to:

    constexpr const char * borderMap[] = { "BORDER_CONSTANT", "BORDER_REPLICATE", "BORDER_REFLECT", "BORDER_WRAP", "BORDER_REFLECT_101" },
        * const btype = borderMap[borderType & ~BORDER_ISOLATED];

which is fine for borderMap but not for btype.

Resulting in:

/Users/sean/external/opencv/modules/imgproc/src/filter.dispatch.cpp:757:17: error: constexpr variable 'btype' must be initialized by a constant expression
  757 |         * const btype = borderMap[borderType & ~BORDER_ISOLATED];
      |                 ^       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@seanm
Copy link

seanm commented Jul 2, 2025

Another interesting case:

static int testToWindowsExtendedPath()
{
#ifdef _WIN32
  // lots of real, non constant work
  return ret;
#else
  return 0;
#endif
}

For some platforms this can be constexpr; for other platforms (Windows), it cannot. It was transformed to constexpr since I'm on Mac. Not sure what you can do there...

@5chmidti
Copy link
Contributor Author

5chmidti commented Jul 11, 2025

I've fixed:

  • the erase_if snippet
  • volatile blocking constexpr
  • docs

Not yet addressed:

  • issues found in VTK
    • DeclStmt
    • duplicate constexprs
    • const removal from lambda param
    • I'll try them next week
static int testToWindowsExtendedPath()
{
#ifdef _WIN32
  // lots of real, non constant work
  return ret;
#else
  return 0;
#endif
}

This will not be possible to detect. The check works once the AST is formed, so after the preprocessor. It can't analyze multiple preprocessor branches at the same time.

I also ran on all of llvm-project/llvm/ without issue

@schenker
Copy link
Contributor

I tested 6040ba1 and found this fixit that does not compile:

Input

namespace
{
struct N
{
 [[maybe_unused]] friend void operator<<(int&, const N&) {}
};
}
clang-tidy -checks="-*,*constexpr*" -export-fixes fixes.yaml test.cpp -- --std=c++23

Result

namespace
{
struct N
{
  constexpr [[maybe_unused]] friend void operator<<(int&, const N&) {}
};
}

The insertion location was broken due to the insertion point being
possibly in front of attributes
Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gave this a very rought look, so only a couple of general questions/suggestions.

Could we do something with methods that repeat in some of the visitors, e.g. bool TraverseType(QualType QT) and others. Maybe macros..? I can't think of a better idea right now.

Could we break this PR in multiple ones, e.g. first with support of CXX11, then CXX14 and so on? 1000-line .cpp file is hard to review. Also, with a PR for each standart we will understand better what changed and what new constexpr cases appear.

Comment on lines +14 to +15
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need all these headers? Seems like DeclBase.h must be a part of Decl.h
Same with Stmt, ASTMatchers etc. IMO we can have indirect includes here, https://llvm.org/docs/CodingStandards.html#include-as-little-as-possible.

return true;
}

static const Type *unwrapPointee(const Type *T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could be placed in ASTUtils.cpp IMO, should be useful in the future.
Or even create a new file TypeUtils.cpp or similar.

Comment on lines +164 to +167
static bool isLiteralType(QualType QT, const ASTContext &Ctx,
const bool ConservativeLiteralType);

static bool isLiteralType(const Type *T, const ASTContext &Ctx,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two could also go in TypeUtils.cpp

@5chmidti
Copy link
Contributor Author

Fixed multiple vars declared in one statement

@5chmidti
Copy link
Contributor Author

Gave this a very rought look, so only a couple of general questions/suggestions.

Could we do something with methods that repeat in some of the visitors, e.g. bool TraverseType(QualType QT) and others. Maybe macros..? I can't think of a better idea right now.

I will try to think of something

Could we break this PR in multiple ones, e.g. first with support of CXX11, then CXX14 and so on? 1000-line .cpp file is hard to review. Also, with a PR for each standart we will understand better what changed and what new constexpr cases appear.

That's a valid point. I'll keep this as is for a while longer without the expectation that people will review it, so that I can resolve a few issues. I also want to fix my setup to be able to run on external code bases. Then I will split it up into {[11],[14, 17],[20],[23, 26]} for 4 reviews. I'm not splitting per version since the two pairs of versions are quite similar to each other. The PRs will probably be serialized and not stacked, because we all know how nice GitHubs PR stacking works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[clang-tidy] suggest adding constexpr when it is possible
8 participants